# 认识 Redux 中间件
中间件相关的信息将作为 createStore 函数的一个 function 类型的入参被传入。这里我们简单复习一下 createStore 的调用规则,示例代码如下:
# 中间件的引入
// 引入 redux
import { createStore, applyMiddleware } from 'redux'
......
// 创建 store
const store = createStore(
reducer,
initial_state,
applyMiddleware(middleware1, middleware2, ...)
);
可以看到,redux 对外暴露了 applyMiddleware 这个方法。applyMiddleware 接受任意个中间件作为入参,而它的返回值将会作为参数传入 createStore,这就是中间件的引入过程
# 中间件的工作模式
中间件的引入,会为 Redux 工作流带来什么样的改变呢?这里我们以 redux-thunk 为例,从经典的“异步 Action”场景切入,一起看看中间件是如何帮我们解决问题的。
redux-thunk——经典的异步 Action 解决方案
在针对 Redux 源码主流程的分析中,我们不难看出这样一个规律——Redux 源码中只有同步操作,也就是说当我们 dispatch action 时,state 会被立即更新
。
那如果想要在 Redux 中引入异步数据流,该怎么办呢?Redux 官方给出的建议是使用中间件来增强 createStore。支持异步数据流的 Redux 中间件有很多,其中最适合用来快速上手的应该就是 redux-thunk了。
redux-thunk 的引入和普通中间件无异,可以参考以下示例:
// 引入 redux-thunk
import thunkMiddleware from 'redux-thunk'
import reducer from './reducers'
// 将中间件用 applyMiddleware 包装后传入
const store = createStore(reducer, applyMiddleware(thunkMiddleware))
// 这里处理的是没有设定初始状态的情况,也就是第一个参数和第二个参数都传 function 的情况
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
// 此时第二个参数会被认为是 enhancer(中间件)
enhancer = preloadedState;
preloadedState = undefined;
}
这段代码告诉我们,在只传入两个参数的情况下,createStore 会去检查第二个参数是否是 function 类型,若是,则认为第二个参数是“enhancer”。这里的“enhancer”是“增强器”的意思,而 applyMiddleware 包装过的中间件,正是“增强器”的一种。这也就解释了为什么上面 redux-thunk 的调用示例中,applyMiddleware 调用明明是作为 createStore 的第二个参数被传入的,却仍然能够被识别为中间件信息
redux-thunk 带来的改变非常好理解,它允许我们以函数的形式派发一个 action,像这样(解析在注释里):
// axios 是一个用于发起异步请求的库
import axios from 'axios'
// 引入 createStore 和 applyMiddleware
import { createStore, applyMiddleware } from 'redux';
// 引入 redux-thunk
import thunk from 'redux-thunk';
// 引入 reducer
import reducer from './reducers';
// 创建一个有 thunk 中间件加持的 store 对象
const store = createStore(
reducer,
applyMiddleware(thunk)
);
// 用于发起付款请求,并处理请求结果。由于涉及资金,我们希望感知请求的发送和响应的返回
// 入参是付款相关的信息(包括用户账密、金额等)
// 注意 payMoney 的返回值仍然是一个函数
const payMoney = (payInfo) => (dispatch) => {
// 付款前发出准备信号
dispatch({ type: 'payStart' })
fetch().then(res => { dispatch()})
return axios.post('/api/payMoney', {
payInfo
})
.then(function (response) {
console.log(response);
// 付款成功信号
dispatch({ type: 'paySuccess' })
})
.catch(function (error) {
console.log(error);
// 付款失败信号
dispatch({ type: 'payError' })
});
}
// 支付信息,入参
const payInfo = {
userName: xxx,
password: xxx,
count: xxx,
......
}
// dispatch 一个 action,注意这个 action 是一个函数
store.dispatch(payMoney(payInfo));
这里我尝试用 redux-thunk
模拟了一个付款请求的发起 → 响应过程。
这个过程单从表面上看,和普通 Redux 调用最大的不同就是 dispatch 的入参从 action 对象变成了一个函数。这就不由得让人对 thunk 中间件加持下的 Redux 工作流心生好奇——action 入参必须是一个对象
要想搞清楚这个问题,你除了需要理解 thunk 的执行逻辑,更重要的是要知道 Redux 中间件是如何工作的。
# Redux 中间件是如何与 Redux 主流程相结合的
Redux 中间件将会在 action
被分发之后、到达 reducer
之前执行,对应到工作流中,它的执行时机如下图所示:
若有多个中间件,那么 Redux 会结合它们被“安装”的先后顺序,依序调用这些中间件,这个过程如下图所示:
中间件的执行时机,允许它在状态真正发生变化之前,结合 action 的信息做一些它想做的事情。
那么中间件又是如何“绕过” dispatch 的校验逻辑的呢?其实,“绕过”dispatch 只是咱们主观上的一个使用感受。dispatch 并非被“绕过”了,而是被“改写”了,改写它的不是别人,正是 applyMiddleware
读到这里,对于 Redux 中间件的工作模式,你需要牢牢把握以下两点
:
- 中间件的执行时机,即
action
被分发之后、reducer
触发之前; - 中间件的执行前提,即
applyMiddleware
将会对dispatch
函数进行改写,使得dispatch
在触发reducer
之前,会首先执行对Redux
中间件的链式调用。
结合这两点,再来看 redux-thunk
的源码,一切就会豁然开朗了。
# thunk 中间件到底做了什么?
// createThunkMiddleware 用于创建 thunk
function createThunkMiddleware(extraArgument) {
// 返回值是一个 thunk,它是一个函数
return ({ dispatch, getState }) => (next) => (action) => {
// thunk 若感知到 action 是一个函数,就会执行 action
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// 若 action 不是一个函数,则不处理,直接放过
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
redux-thunk 主要做的事情,就是在拦截到 action 以后,会去检查它是否是一个函数
。若 action 是一个函数,那么 redux-thunk 就会执行它并且返回执行结果;若 action 不是一个函数,那么它就不是 redux-thunk 的处理目标,直接调用 next,告诉 Redux “我这边的工作做完了”,工作流就可以继续往下走了。
# Redux 中间件机制是如何实现的
Redux 中间件是通过调用 applyMiddleware
来引入的,因此我们先看看 applyMiddleware
的源码
// applyMiddlerware 会使用“...”运算符将入参收敛为一个数组
export default function applyMiddleware(...middlewares) {
// 它返回的是一个接收 createStore 为入参的函数
return createStore => (...args) => {
// 首先调用 createStore,创建一个 store
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
// middlewareAPI 是中间件的入参
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 遍历中间件数组,调用每个中间件,并且传入 middlewareAPI 作为入参,得到目标函数数组 chain
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 改写原有的 dispatch:将 chain 中的函数按照顺序“组合”起来,调用最终组合出来的函数,传入 dispatch 作为入参
dispatch = compose(...chain)(store.dispatch)
// 返回一个新的 store 对象,这个 store 对象的 dispatch 已经被改写过了
return {
...store,
dispatch
}
}
}
在这段源码中,我们着重需要搞清楚的是以下几个问题:
applyMiddleware
返回了一个什么样的函数?这个函数是如何与 createStore 配合工作的?dispatch
函数是如何被改写的?compose
函数是如何组合中间件的?
# 1. applyMiddleware 是如何与 createStore 配合工作的?
先来看看 applyMiddleware 的返回值。在源码的注释中,我已经标明,它返回的是一个接收 createStore 为入参的函数。这个函数将会作为入参传递给 createStore,那么 createStore 会如何理解它呢?这里就要带你复习一下 createStore 中,enhancer 相关的逻辑了,请看下面代码:
function createStore(reducer, preloadedState, enhancer) {
// 这里处理的是没有设定初始状态的情况,也就是第一个参数和第二个参数都传 function 的情况
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
// 此时第二个参数会被认为是 enhancer(中间件)
enhancer = preloadedState;
preloadedState = undefined;
}
// 当 enhancer 不为空时,便会将原来的 createStore 作为参数传入到 enhancer 中
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState);
}
......
}
从这个代码片段中我们可以看出,一旦发现 enhancer 存在(对应到中间件场景下,enhancer 指的是 applyMiddleware 返回的函数),那么 createStore 内部就会直接 return 一个针对 enhancer 的调用。在这个调用中,第一层入参是 createStore,第二层入参是 reducer 和 preloadedState
我们可以尝试将这个逻辑在 applyMiddleware 中对号入座一下。下面我从出入参角度简单提取了一下 applyMiddleware 的源码框架:
// applyMiddlerware 会使用“...”运算符将入参收敛为一个数组
export default function applyMiddleware(...middlewares) {
// 它返回的是一个接收 createStore 为入参的函数
return createStore => (...args) => {
......
}
}
结合 createStore 中对 enhancer 的处理,我们可以知道,在 applyMiddleware return 出的这个函数中,createStore 这个入参对应的是 createStore 函数本身,而 args 入参则对应的是 reducer、preloadedState,这两个参数均为 createStore 函数的约定入参。
前面我们讲过,applyMiddleware 是 enhancer 的一种,而 enhancer 的意思是“增强器”,它增强的正是 createStore 的能力。因此调用 enhancer 时,传入 createStore 及其相关的入参信息是非常必要的。
# 2.dispatch 函数是如何被改写的
dispatch 函数的改写,是由下面这个代码片段完成的:
// middlewareAPI 是中间件的入参
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 遍历中间件数组,调用每个中间件,并且传入 middlewareAPI 作为入参,得到目标函数数组 chain
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 改写原有的 dispatch:将 chain 中的函数按照顺序“组合”起来,调用最终组合出来的函数,传入 dispatch 作为入参
dispatch = compose(...chain)(store.dispatch)
这个代码片段做了两件事:首先以 middlewareAPI
作为入参,逐个调用传入的 middleware
,获取一个由“内层函数”组成的数组 chain;然后调用 compose 函数,将 chain 中的“内层函数”逐个组合起来,并调用最终组合出来的函数。
上面这段描述中,有两个点可能对你的理解构成障碍:
什么是“内层函数”?
- compose 函数到底是怎么组合函数的?它组合出来的又是个什么东西?
- 关于第 2 点,我们需要到 compose 源码中去看,这里先按下不表,咱们来说说“内层函数”在这里的含义。
首先我们需要站在函数的视角,来观察一下 thunk 中间件的源码:
// createThunkMiddleware 用于创建 thunk
function createThunkMiddleware(extraArgument) {
// 返回值是一个 thunk,它是一个函数
return ({ dispatch, getState }) => (next) => (action) => {
// thunk 若感知到 action 是一个函数,就会执行 action
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// 若 action 不是一个函数,则不处理,直接放过
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk 中间件是 createThunkMiddleware
的返回值,createThunkMiddleware 返回的是这样的一个函数:
({ dispatch, getState }) => (next) => (action) => {
// thunk 若感知到 action 是一个函数,就会执行 action
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// 若 action 不是一个函数,则不处理,直接放过
return next(action);
};
该函数的返回值仍然是一个函数,显然它是一个高阶函数。事实上,按照约定,所有的 Redux 中间件都必须是高阶函数。在高阶函数中,我们习惯于将原函数称为“外层函数”,将 return 出来的函数称为“内层函数”。
而 apply 中遍历 middlewares 数组,逐个调用 middleware(middlewareAPI)
,无非是为了获取中间件的内层函数。
以 thunk 的源码为例,不难看出,外层函数的主要作用是获取 dispatch、getState 这两个 API,而真正的中间件逻辑是在内层函数中包裹的。待middlewares.map(middleware => middleware(middlewareAPI))
执行完毕后,内层函数会被悉数提取至 chain 数组。接下来,我们直接拿 chain 数组开刀就行了
提取出 chain 数组之后,applyMiddleware 做的第一件事就是将数组中的中间件逻辑 compose 起来。
那么 compose 函数又是如何工作的呢?
# 3. compose 源码解读:函数的合成
函数合成(组合函数)并不是 Redux 的专利,而是函数式编程中一个通用的概念。因此在 Redux 源码中,compose 函数是作为一个独立文件存在的,它具备较强的工具属性。
我们还是先通过阅读源码,来弄清楚 compose 到底都做了什么。以下是 compose 的源码(解析在注释里):
// compose 会首先利用“...”运算符将入参收敛为数组格式
export default function compose(...funcs) {
// 处理数组为空的边界情况
if (funcs.length === 0) {
return arg => arg
}
// 若只有一个函数,也就谈不上组合,直接返回
if (funcs.length === 1) {
return funcs[0]
}
// 若有多个函数,那么调用 reduce 方法来实现函数的组合
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
其实整段源码中值得你细细品味的只有最后这一行代码:
// 若有多个函数,那么调用 reduce 方法来实现函数的组合
return funcs.reduce((a, b) => (...args) => a(b(...args)))
这行代码告诉我们,函数组合是通过调用数组的 reduce 方法来实现的。
reducer 方法的特点是,会对数组中的每个元素执行我们指定的函数逻辑,并将其结果汇总为单个返回值。因此对于这样的一个 compose 调用来说:
compose(f1, f2, f3, f4)
它会把函数组合为这种形式:
(...args) => f1(f2(f3(f4(...args))))
如此一来,f1、f2、f3、f4 这 4 个中间件的内层逻辑就会被组合到一个函数中去,当这个函数被调用时,f1、f2、f3、f4 将会按照顺序被依次调用。这就是“函数组合”在此处的含义